Skip to content

20260221 #221 비밀번호 초기화 api 구현#233

Merged
discipline24 merged 13 commits intomainfrom
20260221_#221_비밀번호-초기화-API-구현
Feb 22, 2026

Hidden character warning

The head ref may contain hidden characters: "20260221_#221_\ube44\ubc00\ubc88\ud638-\ucd08\uae30\ud654-API-\uad6c\ud604"
Merged

20260221 #221 비밀번호 초기화 api 구현#233
discipline24 merged 13 commits intomainfrom
20260221_#221_비밀번호-초기화-API-구현

Conversation

@daye200
Copy link
Contributor

@daye200 daye200 commented Feb 21, 2026

#221

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 출석 라운드의 QR 코드 토큰을 실시간으로 스트리밍하는 새 엔드포인트 추가
    • 비밀번호 재설정 프로세스 개선: 학번 기반 인증 강화
  • 버그 수정

    • 비밀번호 재설정 코드 만료 및 유효성 검사 오류 처리 개선
  • 문서

    • 출석 관리 API 엔드포인트 설명서 상세화

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Warning

Rate limit exceeded

@daye200 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 30 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

비밀번호 재설정 기능이 Redis 기반의 코드 검증 방식으로 재설계되었으며, 이메일 재검증 단계가 제거되었습니다. 또한 QR 토큰 스트리밍 엔드포인트가 추가되었고, 보안 화이트리스트와 API 문서가 업데이트되었습니다. 사용자 저장소에 이메일과 학번을 기반으로 한 쿼리 메서드가 추가되었습니다.

Changes

Cohort / File(s) Summary
OpenAPI 문서화 업데이트
backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java, backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java
API 엔드포인트의 OpenAPI @Operation 주석을 상세한 설명, 권한 요구사항, 요청/응답 본문으로 확장하여 문서화 개선.
QR 토큰 스트리밍 기능
backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java
QrTokenStreamService 의존성을 추가하고, GET /api/attendance/rounds/{roundId}/qr-stream SSE 엔드포인트를 구현하여 실시간 QR 토큰 스트리밍 기능 제공.
비밀번호 재설정 플로우 재설계
backend/src/main/java/org/sejongisc/backend/user/service/UserService.java, backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java, backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java
토큰 기반에서 Redis 기반 코드 검증으로 변경. 이메일 검증 단계 제거. POST /password/reset/confirm 엔드포인트 추가. passwordResetSendCode와 resetPasswordByCode 메서드 추가.
비밀번호 재설정 요청 DTO 확장
backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java, backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetConfirmRequest.java
PasswordResetSendRequest에 studentId 필드 추가. PasswordResetVerifyRequest를 PasswordResetConfirmRequest로 이름 변경하고, studentId 및 newPassword 필드 추가.
저장소 쿼리 메서드 추가
backend/src/main/java/org/sejongisc/backend/user/repository/UserRepository.java
existsByEmailAndStudentId 및 findByEmailAndStudentId 메서드 추가하여 이메일과 학번 조합으로 사용자 조회 가능하게 함.
설정 및 예외 처리 확장
backend/src/main/java/org/sejongisc/backend/common/config/EmailProperties.java, backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java, backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java
EmailProperties.KeyPrefix에 reset 필드 추가. WHITELIST_URLS에 /api/user/password/reset/confirm/api/user/password/reset/send 추가(쉼표 누락 주의). RESET_CODE_EXPIRED 및 INVALID_RESET_CODE ErrorCode 상수 추가.
테스트 커버리지 조정
backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java
OAuth 관련 테스트 및 기존 비밀번호 재설정 테스트를 주석 처리. signup_nullRole_defaultsToMember 테스트는 유지.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant UC as UserController
    participant US as UserService
    participant UR as UserRepository
    participant ES as EmailService
    participant Redis as Redis
    participant DB as Database

    Note over Client,DB: 비밀번호 재설정 코드 전송
    Client->>UC: POST /password/reset/send<br/>(email, studentId)
    UC->>US: passwordResetSendCode(req)
    US->>UR: existsByEmailAndStudentId(email, studentId)
    UR-->>US: true/false
    alt User Exists
        US->>ES: sendResetEmail(email)
        ES->>Redis: SET reset:{email} code (with TTL)
        Redis-->>ES: OK
        ES-->>US: Success
        US-->>UC: 코드 전송 완료
        UC-->>Client: 200 OK
    else User Not Found
        US-->>UC: Exception
        UC-->>Client: 400/404 Error
    end

    Note over Client,DB: 비밀번호 재설정 코드 확인 및 변경
    Client->>UC: POST /password/reset/confirm<br/>(code, newPassword, email, studentId)
    UC->>US: resetPasswordByCode(code, newPassword, req)
    US->>UR: findByEmailAndStudentId(email, studentId)
    UR-->>US: Optional<User>
    alt User Found
        US->>Redis: GET reset:{email}
        Redis-->>US: savedCode
        alt Code Match & Not Expired
            US->>DB: Update user password
            DB-->>US: Updated
            US->>Redis: DEL reset:{email}
            Redis-->>US: Deleted
            US-->>UC: Success
            UC-->>Client: 200 OK
        else Code Expired
            US-->>UC: RESET_CODE_EXPIRED
            UC-->>Client: 400 Bad Request
        else Code Invalid
            US-->>UC: INVALID_RESET_CODE
            UC-->>Client: 400 Bad Request
        end
    else User Not Found
        US-->>UC: Exception
        UC-->>Client: 400/404 Error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • discipline24

Poem

🐰 Redis 저장소에 코드를 숨겨두고,
학번과 이메일로 사용자를 찾아내며,
QR 토큰은 흐르고 흘러 스트리밍되네.
보안이 한 층 더 단단해지는 밤,
재설정 기능이 우아하게 춤을 춘다! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 비밀번호 초기화 API 구현이라는 PR의 주요 변경사항을 명확하게 반영하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 20260221_#221_비밀번호-초기화-API-구현

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java (1)

339-346: ⚠️ Potential issue | 🟠 Major

비밀번호 재설정 플로우에 대한 테스트가 누락되었습니다.

이 PR에서 UserServicepasswordResetSendCoderesetPasswordByCode 메서드가 추가되었으나, 해당 기능에 대한 단위 테스트가 전혀 없습니다. 최소한 다음 시나리오에 대한 테스트가 필요합니다:

  • 정상적인 코드 발송 성공
  • 존재하지 않는 이메일+학번 조합
  • 유효한 코드로 비밀번호 재설정 성공
  • 만료/잘못된 코드로 재설정 실패

비밀번호 재설정 테스트 코드를 생성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`
around lines 339 - 346, Add unit tests in UserServiceTest covering the
password-reset flow for the newly added UserService methods
passwordResetSendCode and resetPasswordByCode: create tests that (1) verify
successful code sending for a valid email+studentId by mocking the
UserRepository to return a user and asserting MailService.send (or equivalent)
is called and any code store is populated, (2) verify sending fails (and
appropriate exception is thrown) when email+studentId does not exist, (3) verify
successful resetPasswordByCode when a valid code is provided by mocking the code
lookup to match and asserting PasswordEncoder.encode is used and
UserRepository.save is called with updated password, and (4) verify
resetPasswordByCode throws for expired/invalid codes by mocking the code lookup
to return no match or expired entry; reference
UserService.passwordResetSendCode, UserService.resetPasswordByCode,
UserRepository, MailService (or mail sending collaborator), and PasswordEncoder
when wiring mocks and assertions.
backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java (1)

10-10: ⚠️ Potential issue | 🟠 Major

와일드카드 패턴 /api/user/password/reset/**가 이미 모든 하위 경로를 허용하여 Line 22-23이 중복됩니다.

Line 10의 와일드카드 패턴이 이미 /api/user/password/reset/confirm/api/user/password/reset/send를 포함하므로 Line 22-23은 불필요합니다.

더 중요한 문제는, 와일드카드 패턴이 향후 해당 경로 하위에 추가되는 인증이 필요한 엔드포인트까지 의도치 않게 화이트리스트에 포함시킬 수 있다는 점입니다. 와일드카드를 제거하고 명시적 경로만 유지하는 것이 안전합니다.

🛡️ 제안
-            "/api/user/password/reset/**",

Also applies to: 22-23

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java`
at line 10, 보안 화이트리스트에 있는 와일드카드 패턴 "/api/user/password/reset/**"가 이미
"/api/user/password/reset/confirm" 및 "/api/user/password/reset/send"를 포함해 중복되므로,
SecurityConstants 클래스에서 해당 와일드카드 항목을 제거하고 대신 명시적 경로만 유지하도록 수정하세요: 삭제 대상은
"/api/user/password/reset/**"이고 유지/추가할 명시적 항목은
"/api/user/password/reset/confirm" 및 "/api/user/password/reset/send"이며 중복된 항목(현재
22-23번에 해당하는 라인)은 제거해 향후 하위 엔드포인트가 의도치 않게 화이트리스트에 포함되는 것을 방지하세요.
backend/src/main/java/org/sejongisc/backend/user/service/UserService.java (1)

168-174: ⚠️ Potential issue | 🟡 Minor

log.debug(fieldName) — 로그 메시지가 불완전합니다.

Line 170에서 fieldName만 단독으로 로그에 출력하고 있어, 어떤 상황에서 어떤 필드가 유효하지 않은지 파악하기 어렵습니다.

🛠️ 제안
-           log.debug(fieldName);
+           log.debug("유효성 검증 실패: {}가 비어있습니다.", fieldName);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`
around lines 168 - 174, The log call in validateNotBlank currently logs only
fieldName (log.debug(fieldName)), which is not descriptive; update the logging
to include a clear message and the field identifier (e.g., "Validation failed:
field is blank - {}" with fieldName) so it's obvious which field failed before
throwing CustomException(ErrorCode.INVALID_INPUT); keep using validateNotBlank
as the location and ensure log.debug includes both context and the fieldName
(and optionally the trimmed value or null) for better diagnostics.
🧹 Nitpick comments (20)
backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java (1)

30-32: 로컬 환경 설정은 주석 코드 대신 Spring Profile 기반으로 분리하는 것을 권장합니다.

커밋된 주석 코드(secure(false), sameSite("Lax"))는 코드 가독성을 저해하며, 실수로 활성화될 위험이 있습니다. 또한 deleteCookie에는 동일한 로컬 오버라이드가 없어 로컬 테스트 시 쿠키 삭제가 여전히 동작하지 않을 수 있습니다.

Spring Profile(application-local.yml + @Profile("local"))이나 @Value 프로퍼티를 활용해 환경별 설정을 분리하는 것을 권장합니다.

♻️ Spring Profile 기반 리팩토링 예시

application-local.yml (또는 application.properties)에 프로퍼티 추가:

# application-local.yml
cookie:
  secure: false
  same-site: Lax

# application.yml (기본)
cookie:
  secure: true
  same-site: None

AuthCookieHelper.java 수정:

 `@Component`
 public class AuthCookieHelper {
 
+    `@Value`("${cookie.secure:true}")
+    private boolean cookieSecure;
+
+    `@Value`("${cookie.same-site:None}")
+    private String cookieSameSite;
+
     private ResponseCookie createCookie(String name, String value, long maxAge) {
         return ResponseCookie.from(name, value)
                 .httpOnly(true)
-                // 로컬에서
-//            .secure(false)
-//            .sameSite("Lax")
-                .secure(true)
-                .sameSite("None")
+                .secure(cookieSecure)
+                .sameSite(cookieSameSite)
                 .path("/")
                 .maxAge(maxAge)
                 .build();
     }
 
     public ResponseCookie deleteCookie(String name) {
         return ResponseCookie.from(name, "")
                 .httpOnly(true)
-                .secure(true)
-                .sameSite("None")
+                .secure(cookieSecure)
+                .sameSite(cookieSameSite)
                 .path("/")
                 .maxAge(0)
                 .build();
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java`
around lines 30 - 32, Replace the commented local-only cookie settings in
AuthCookieHelper by externalizing cookie config into environment-specific
properties (e.g., add cookie.secure and cookie.same-site in application.yml /
application-local.yml) and read them via `@Value` or a `@ConfigurationProperties`
bean; then apply those properties wherever cookies are created/removed
(references: AuthCookieHelper, deleteCookie, the secure(...) and sameSite(...)
usages) so local profile overrides (`@Profile`("local") or application-local.yml)
control secure/same-site behavior instead of committing commented code.
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (2)

171-172: 매직 넘버 5L을 상수로 추출하는 것을 권장합니다

기본 지각 기준 시간(5분)이 상수 없이 인라인으로 작성되어 있어 의미를 파악하기 어렵고, 추후 변경 시 누락될 수 있습니다.

♻️ 상수 추출 예시
+  private static final long DEFAULT_LATE_THRESHOLD_MINUTES = 5L;
+
   private boolean decideLate(AttendanceRound round, LocalDateTime checkedAt) {
     Integer allowedMinutes =
         round.getAttendanceSession() != null ? round.getAttendanceSession().getAllowedMinutes() : null;

-    long minutes = (allowedMinutes == null || allowedMinutes <= 0) ? 5L : allowedMinutes.longValue();
+    long minutes = (allowedMinutes == null || allowedMinutes <= 0) ? DEFAULT_LATE_THRESHOLD_MINUTES : allowedMinutes.longValue();
     var threshold = round.getStartAt().plusMinutes(minutes);
     return checkedAt.isAfter(threshold);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java`
around lines 171 - 172, Extract the magic literal 5L used to compute the default
allowed minutes into a descriptive constant (e.g. private static final long
DEFAULT_ALLOWED_MINUTES = 5L) and replace the inline 5L in AttendanceService
where minutes is computed ((allowedMinutes == null || allowedMinutes <= 0) ?
DEFAULT_ALLOWED_MINUTES : allowedMinutes.longValue()), leaving the subsequent
use of round.getStartAt().plusMinutes(minutes) unchanged.

168-169: getAttendanceSession() 널 가드가 다른 호출 지점과 일관성이 없습니다

decideLate에서는 round.getAttendanceSession() != null을 확인하지만, 같은 클래스의 다른 메서드에서는 해당 검사 없이 직접 접근합니다.

  • Line 46 (checkIn): round.getAttendanceSession().getAttendanceSessionId()
  • Line 93 (getAttendancesByRound): round.getAttendanceSession().getAttendanceSessionId()
  • Line 115 (updateAttendanceStatusByRound): round.getAttendanceSession().getAttendanceSessionId()

AttendanceSession이 실제로 null이 될 수 있다면 위 세 지점도 NPE 위험에 노출됩니다. 반대로 항상 존재가 보장된다면 decideLate의 null 검사는 불필요한 코드로, 향후 유지보수 시 혼란을 줄 수 있습니다. 엔티티 관계의 실제 제약 조건에 맞춰 일관된 방어 수준을 적용해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java`
around lines 168 - 169, Decide on the true nullability of AttendanceSession and
make accesses consistent: if AttendanceSession is guaranteed non-null by the
entity mapping, remove the redundant null check in decideLate and replace it
with a non-null contract (e.g., use Objects.requireNonNull or let it surface as
an NPE) so checkIn, getAttendancesByRound, updateAttendanceStatusByRound and
decideLate all access round.getAttendanceSession() directly; otherwise, if it
can be null, add the same null-guard/handling currently in decideLate to
checkIn, getAttendancesByRound, and updateAttendanceStatusByRound (or throw a
clear IllegalStateException) so every method (checkIn, getAttendancesByRound,
updateAttendanceStatusByRound, decideLate) uniformly handles
round.getAttendanceSession() being null.
backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java (1)

61-89: LGTM — 문서화 잘 되어 있습니다. 다만 컨트롤러 레벨 권한 강제를 고려해 보세요.

Swagger 설명(Line 69, 83)에서 세션 관리자/OWNER만 호출 가능하다고 명시하고 있으나, 해당 메서드에는 @PreAuthorize 등의 어노테이션이 없어 권한 검증이 전적으로 서비스 레이어에 위임되어 있습니다. 서비스 로직이 리팩토링될 경우 권한 우회 가능성이 생길 수 있습니다.

🔒 컨트롤러 레벨 권한 어노테이션 추가 제안
+import org.springframework.security.access.prepost.PreAuthorize;
 ...
+@PreAuthorize("hasAnyRole('ADMIN', 'OWNER')")
 `@PutMapping`("/rounds/{roundId}/users/{userId}")
 public ResponseEntity<AttendanceResponse> updateAttendanceStatus(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java`
around lines 61 - 89, The controller method annotated with the "출석 상태 수정"
`@Operation` in AttendanceController lacks a controller-level security check; add
a Spring Security annotation (e.g., `@PreAuthorize`) to that controller method to
enforce only session administrators/OWNER can call it (use the same
role/authority names used elsewhere in the project, e.g., hasRole('OWNER') or
hasAuthority('SESSION_ADMIN')). Import the PreAuthorize annotation and place it
directly above the method that contains the shown Operation documentation so
authorization is enforced at the controller layer rather than relying solely on
the service layer.
backend/build.gradle (1)

71-73: spring-boot-starter-quartz 의존성 추가 — LGTM, 사소한 포맷 개선 여지 있음

버전을 명시하지 않고 Spring Boot BOM에 위임한 것은 올바른 방식입니다.

다만 72~73 라인에 빈 줄이 두 개 연속 삽입되어 있습니다. 파일 내 다른 의존성 그룹 구분은 모두 빈 줄 한 개를 사용하므로, 일관성을 위해 하나를 제거하는 것이 좋습니다.

🔧 포맷 정리 제안
 	implementation 'org.springframework.boot:spring-boot-starter-quartz'
-
-
+
 	// Actuator
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/build.gradle` around lines 71 - 73, After adding implementation
'org.springframework.boot:spring-boot-starter-quartz', there are two consecutive
blank lines; remove one so there is only a single blank line following that
dependency to match the file's existing dependency-group spacing and keep
formatting consistent around the implementation
'org.springframework.boot:spring-boot-starter-quartz' line.
backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java (2)

5-8: Server 임포트 추가에도 불구하고 FQCN이 사용됨 / Info 임포트 미사용

Line 8에서 Server를 임포트했지만 Line 33, 36에서 여전히 io.swagger.v3.oas.models.servers.Server와 같은 완전한 클래스명(FQCN)을 사용하고 있어 임포트가 실질적으로 적용되지 않습니다. 임포트를 활용하도록 수정하면 코드가 간결해집니다.

또한 Line 5에서 Info를 임포트했지만 openAPI() 빌더에 .info(...) 호출이 없어 사용되지 않았습니다. API 제목·버전·설명 등의 정보를 추가할 의도가 있었던 것인지 확인이 필요합니다.

♻️ FQCN → 임포트된 타입으로 교체 제안
  return new OpenAPI()
      .components(new Components().addSecuritySchemes("Bearer Token", apiKey))
      .addSecurityItem(securityRequirement)
      .servers(List.of(
-             new io.swagger.v3.oas.models.servers.Server()
+             new Server()
                  .url("http://localhost:8080")
                  .description("로컬 서버"),
-             new io.swagger.v3.oas.models.servers.Server()
+             new Server()
                  //.url("https://api.sisc.com") // TODO: url 수정 필요
                  .description("메인 서버")
          )
      );

Info를 사용하려면 아래와 같이 빌더 체인에 추가하세요:

  return new OpenAPI()
+     .info(new Info()
+         .title("SISC API")
+         .version("1.0.0"))
      .components(new Components().addSecuritySchemes("Bearer Token", apiKey))

Info 구성 추가가 필요하다면 제목·버전·설명 등을 포함한 구현을 새 이슈로 생성해 드릴까요?

Also applies to: 33-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java`
around lines 5 - 8, In SwaggerConfig, replace the fully-qualified Server usages
in the openAPI() builder with the imported Server type (use new Server() instead
of io.swagger.v3.oas.models.servers.Server) so the import of Server is actually
used, and either add an .info(...) call using the imported Info (e.g., .info(new
Info().title(...).version(...).description(...)) in openAPI()) or remove the
unused Info import if API metadata is not needed; update the openAPI() method
accordingly to reflect one of these two choices.

36-38: 메인 서버 URL이 주석 처리되어 있어 Swagger UI에 빈 URL로 표시될 수 있음

.url()이 주석 처리된 채로 두면 Swagger UI의 서버 드롭다운에 "메인 서버"에 대해 빈값 또는 null이 표시됩니다. 배포 환경의 URL이 확정될 때까지 해당 Server 항목 자체를 제거하거나, 플레이스홀더 URL을 유지하는 것을 권장합니다.

환경별(local / prod) 서버 URL을 프로파일 기반으로 주입하는 구성이 필요하시면 구현을 도와드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java`
around lines 36 - 38, In SwaggerConfig, the new
io.swagger.v3.oas.models.servers.Server() is left with .url() commented out
which causes the Swagger UI to show a blank/null server; fix it by either
removing that Server entry or setting a placeholder/prod URL and preferably
wiring it from configuration: add a String serverUrl injected via
`@Value`("${swagger.server.url:}") (or from Environment/profile) and call
.url(serverUrl) when building the Server instance in the OpenAPI/OpenAPI bean
initializer in SwaggerConfig; ensure the server is omitted entirely if serverUrl
is empty or set a clear placeholder like "http://localhost:8080" for dev
profiles.
backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java (1)

135-168: 주석 처리된 테스트 코드를 정리하세요.

주석 처리된 테스트를 유지하는 것보다 삭제하고 필요 시 Git 이력에서 복원하는 것이 좋습니다. 주석 처리된 코드는 유지보수를 어렵게 만듭니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`
around lines 135 - 168, Remove the entire commented-out test block for
signup_nullRole_defaultsToMember in UserServiceTest: delete the commented `@Test`
method (including the SignupRequest setup, mocked when(...) stubs, save Answer
lambda, and assertions) and commit the cleanup; if the test is needed later,
recover it from Git history rather than leaving it commented in the file, and
ensure any behavior around userService.signup and Role defaults is covered by an
active test instead.
backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java (1)

132-145: sendResetEmail에서 에러 로깅이 누락되었습니다.

기존 sendEmail 메서드(Line 89)에서는 log.error로 실패를 로깅하지만, sendResetEmail에서는 로깅 없이 바로 예외를 던집니다. 장애 추적을 위해 동일하게 로깅을 추가하세요.

♻️ 제안
     } catch (MessagingException | MailException e) {
+      log.error("비밀번호 재설정 메일 전송 실패", e);
       redisTemplate.delete(key);
       throw new MailSendException("failed to send mail", e);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java`
around lines 132 - 145, Add the same error logging used in sendEmail to
sendResetEmail: inside the catch block for MessagingException | MailException in
sendResetEmail, call log.error with a descriptive message and the caught
exception (e.g., "Failed to send reset email for {}" with the email and
exception e) before deleting the Redis key and throwing the MailSendException;
reference sendResetEmail, createResetMessage, redisTemplate, mailSender,
MailSendException and reuse the logging pattern used in sendEmail.
backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java (1)

16-17: studentId@Schema 어노테이션이 누락되었습니다.

email 필드에는 @Schema가 설정되어 있는 반면, studentId에는 누락되어 Swagger 문서의 일관성이 떨어집니다.

♻️ 제안
-        `@NotBlank`(message = "학번은 필수입니다.")
-        String studentId
+        `@Schema`(
+                example = "20230001",
+                description = "사용자의 학번"
+        )
+        `@NotBlank`(message = "학번은 필수입니다.")
+        String studentId
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java`
around lines 16 - 17, The PasswordResetSendRequest DTO is missing a `@Schema`
annotation on the studentId field which breaks Swagger consistency; update the
PasswordResetSendRequest class to annotate the studentId field (String
studentId) with a `@Schema` providing a description, example and required flag
consistent with the existing email field's `@Schema` usage so OpenAPI docs show
the same metadata for both fields.
backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetConfirmRequest.java (1)

26-27: studentIdnewPassword@Schema 어노테이션이 누락되었습니다.

email, code 필드에는 @Schema가 있지만 나머지 필드에는 없어 Swagger 문서 일관성이 떨어집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetConfirmRequest.java`
around lines 26 - 27, PasswordResetConfirmRequest is missing `@Schema` annotations
on the studentId and newPassword fields; add `@Schema`(...) to both fields
consistent with the existing email and code annotations (providing a description
and example as used for email/code) and ensure the
io.swagger.v3.oas.annotations.media.Schema import is present so Swagger docs
remain consistent for the class.
backend/src/main/java/org/sejongisc/backend/user/service/UserService.java (2)

104-113: 인증코드 발송 로직은 전반적으로 적절합니다.

이메일+학번 조합으로 사용자 존재 여부를 확인한 뒤 이메일을 발송하는 흐름이 명확합니다.

다만, Line 109에서 이메일과 학번을 로그에 기록하고 있습니다. 디버그 레벨이라 해도 PII(개인식별정보)를 로그에 남기는 것은 개인정보보호 측면에서 리스크가 있습니다. 마스킹 처리를 권장합니다.

♻️ PII 마스킹 제안
-        log.debug("이메일과 학번 불일치: email={}, studentId={}", nEmail, nStudentId);
+        log.debug("이메일과 학번 불일치: email={}***, studentId={}***",
+            nEmail.substring(0, Math.min(3, nEmail.length())),
+            nStudentId.substring(0, Math.min(2, nStudentId.length())));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`
around lines 104 - 113, In passwordResetSendCode, avoid logging raw PII in the
debug log; change the log.debug call that currently references nEmail and
nStudentId (after userRepository.existsByEmailAndStudentId check) to log masked
values instead. Implement or call a small masking helper (e.g., maskEmail and
maskStudentId) to obfuscate the local-part and/or domain of nEmail and to
partially hide nStudentId (e.g., show only last 2 characters), then pass those
masked strings into log.debug so you still get useful context without storing
plaintext PII.

176-203: 사용되지 않는 레거시 메서드와 불필요한 캐스트를 제거하세요.

saveResetTokenToRedis, getEmailFromRedis, deleteResetTokenFromRedis 세 메서드는 더 이상 호출되지 않는 코드입니다. 또한 Line 188의 (String) 캐스트는 RedisTemplate<String, String>으로 이미 제너릭 타입이 정해져 있어 불필요합니다.

데드 코드를 제거하여 코드 가독성을 개선하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`
around lines 176 - 203, Remove the dead Redis helper methods
saveResetTokenToRedis, getEmailFromRedis, and deleteResetTokenFromRedis from
UserService since they are no longer used; before deleting, search for any
callers and update or remove them if found, then run a build to ensure no
references remain. Also remove the unnecessary cast in getEmailFromRedis (the
(String) cast) if you decide to keep similar logic elsewhere, and ensure
redisTemplate is typed as RedisTemplate<String,String> where appropriate.
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java (1)

140-146: 두 개의 catch 블록을 하나로 합칠 수 있습니다.

IllegalArgumentExceptionIllegalStateException 모두 동일하게 QR_TOKEN_MALFORMED로 매핑되므로, multi-catch 구문으로 간소화할 수 있습니다.

제안된 수정
     try {
       RollingQrTokenUtil.verifyAndParse(qrToken, round.getQrSecret());
-    } catch (IllegalArgumentException e) {
-      throw new CustomException(ErrorCode.QR_TOKEN_MALFORMED);
-    } catch (IllegalStateException e) {
+    } catch (IllegalArgumentException | IllegalStateException e) {
       throw new CustomException(ErrorCode.QR_TOKEN_MALFORMED);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java`
around lines 140 - 146, In AttendanceRoundService, inside the try block that
calls RollingQrTokenUtil.verifyAndParse(qrToken, round.getQrSecret()), replace
the two separate catch blocks for IllegalArgumentException and
IllegalStateException with a single multi-catch (IllegalArgumentException |
IllegalStateException e) that throws new
CustomException(ErrorCode.QR_TOKEN_MALFORMED); this simplifies the code while
preserving identical error handling for both exceptions.
backend/src/main/java/org/sejongisc/backend/attendance/util/RollingQrTokenUtil.java (3)

94-103: hmacSha256Base64Url의 에러 메시지가 실제 상황을 반영하지 않습니다.

Line 101에서 HMAC 서명 과정의 실패를 "QR_TOKEN_MALFORMED"로 던지고 있습니다. 이 메시지는 토큰 파싱 오류와 혼동될 수 있으며, 디버깅 시 원인 파악을 어렵게 만듭니다. "QR_SIGN_ERROR" 등 서명 실패에 맞는 메시지 사용을 권장합니다. (참고: 기존 QrTokenUtil에서는 QR_SIGN_ERROR를 사용하고 있습니다.)

제안된 수정
     } catch (Exception e) {
-      throw new IllegalStateException("QR_TOKEN_MALFORMED", e);
+      throw new IllegalStateException("QR_SIGN_ERROR", e);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/util/RollingQrTokenUtil.java`
around lines 94 - 103, Update the exception message thrown in
RollingQrTokenUtil.hmacSha256Base64Url to reflect a signing failure instead of
token parsing: replace the IllegalStateException message "QR_TOKEN_MALFORMED"
with a signing-specific message like "QR_SIGN_ERROR" (matching QrTokenUtil) so
callers can distinguish HMAC/signing errors from token parsing errors; keep the
original exception as the cause when rethrowing.

82-92: toWindowExpiresAtbuildTokenpublic으로 노출되어 있습니다.

이 두 메서드는 내부 토큰 생성 로직의 세부사항입니다. QrTokenStreamService에서 toWindowExpiresAt을 사용하고 있으므로 필요에 따라 public 유지가 가능하지만, buildToken은 외부에서 직접 호출 시 잘못된 expiresAtEpochSec 값으로 위조 가능한 토큰이 만들어질 수 있습니다. 접근 제어를 package-private으로 축소하는 것을 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/util/RollingQrTokenUtil.java`
around lines 82 - 92, Reduce the exposure of internal token creation logic by
changing the access modifier of buildToken so it is package-private (remove
public) to prevent external callers from forging tokens with arbitrary expiresAt
values; leave toWindowExpiresAt as-is (or make package-private only if
QrTokenStreamService does not require public access) and update any related
Javadoc/comments accordingly; locate the methods by their names to apply the
modifier change: buildToken(...) and toWindowExpiresAt(...), and ensure
tests/uses in QrTokenStreamService still compile after the change.

105-113: constantTimeEquals 구현이 적절합니다.

HMAC-SHA256 Base64URL 인코딩 결과는 항상 동일한 길이이므로, Line 107의 길이 비교에서 타이밍 정보가 누출되더라도 실질적인 보안 위험은 없습니다. 다만, Java 표준 라이브러리인 MessageDigest.isEqual(byte[], byte[]) 사용을 고려하면 직접 구현 없이 동일한 보장을 얻을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/util/RollingQrTokenUtil.java`
around lines 105 - 113, Replace the custom constant-time comparison in
constantTimeEquals with Java's built-in constant-time method: convert the two
input Strings to byte[] using a stable charset (e.g., StandardCharsets.UTF_8)
and call MessageDigest.isEqual(byte[], byte[]) to perform the comparison; keep
the null checks and length handling as needed (or rely on MessageDigest.isEqual
after null/length checks) so callers of constantTimeEquals (the method
constantTimeEquals) get the same constant-time guarantee without a custom
bitwise loop.
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundStateJob.java (1)

23-30: 예외가 로그만 남기고 무시됩니다.

현재 구현에서는 runRoundStatusMaintenance() 실패 시 예외를 로그로만 남기고 JobExecutionException을 던지지 않습니다. 이는 Quartz가 실패를 인지하지 못하므로 재실행 정책이 적용되지 않습니다. 매 30분마다 다시 실행되므로 의도적인 설계일 수 있지만, 운영 환경에서 연속 실패를 모니터링하기 위해 실패 카운터나 알림 추가를 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundStateJob.java`
around lines 23 - 30, 현재 AttendanceRoundStateJob.execute에서
attendanceRoundService.runRoundStatusMaintenance() 실패 시 예외를 로그만 남기고 무시하고 있으니,
catch 블록에서 로그 후 Quartz가 인지하도록 JobExecutionException을 던지도록 수정하고(예: throw new
JobExecutionException(e)), 필요하면 실패 카운터를 증가시키거나 알림을 트리거하는 로직을 추가해 운영 모니터링에 반영하세요;
대상 식별자: AttendanceRoundStateJob.execute,
attendanceRoundService.runRoundStatusMaintenance, JobExecutionException.
backend/src/main/java/org/sejongisc/backend/attendance/config/AttendanceQuartzConfig.java (1)

48-60: createBean은 새 인스턴스를 매번 생성하므로 AttendanceRoundStateJob@Component가 불필요합니다.

AutowiringSpringBeanJobFactory.createJobInstance에서 beanFactory.createBean(jobClass)를 호출하면 매 실행마다 새 인스턴스가 생성되고 의존성이 주입됩니다. 이는 AttendanceRoundStateJob에 선언된 @Component로 생성되는 Spring 싱글톤과 별개입니다. Quartz는 이 싱글톤을 사용하지 않으므로, @Component 어노테이션을 제거하면 불필요한 빈 등록을 방지할 수 있습니다.

#!/bin/bash
# Verify if `@Component` on AttendanceRoundStateJob is referenced anywhere else
rg -n "AttendanceRoundStateJob" --type java -C 3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/config/AttendanceQuartzConfig.java`
around lines 48 - 60, AutowiringSpringBeanJobFactory.createJobInstance calls
beanFactory.createBean(jobClass) which creates a new instance per execution so
the AttendanceRoundStateJob `@Component` is redundant and registers an unused
singleton; remove the `@Component` annotation from the AttendanceRoundStateJob
class (or change its scope to prototype if you intentionally want singleton vs
per-execution behavior) and verify no other code depends on the bean by
searching for usages of AttendanceRoundStateJob (e.g., rg -n
"AttendanceRoundStateJob"); leave
AutowiringSpringBeanJobFactory.createJobInstance and beanFactory.createBean
as-is.
backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java (1)

47-47: 스레드 풀 크기 4가 하드코딩되어 있습니다.

라운드당 2개의 스케줄 작업(토큰 브로드캐스트 + 핑)이 필요하므로, 동시에 활성화되는 라운드가 많아지면 4개 스레드로는 부족할 수 있습니다. 특히 scheduleAtFixedRate에서 스레드가 모두 점유되면 토큰 발급이 지연될 수 있습니다. 설정값으로 외부화하거나 활성 라운드 수에 비례하여 조정하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java`
at line 47, 필드 scheduler(실타입 ScheduledExecutorService)를 생성할 때 하드코딩된 크기 4를 사용하지
말고 구성값 또는 활성 라운드 수에 따라 동적으로 결정하도록 변경하세요; 예를 들어 QrTokenStreamService에서
Executors.newScheduledThreadPool(...) 호출을 설정값(application properties 또는 생성자
인자)으로 대체하거나 활성 라운드 수 * 2(라운드당 토큰 브로드캐스트와 핑)와 같은 식으로 계산하여 스레드풀 크기를 설정하도록 수정하고, 기존
scheduleAtFixedRate를 사용하는 스케줄링 로직은 그대로 두되 스레드풀이 부족해 지연되지 않도록 생성자 또는 팩토리에서
scheduler를 주입/초기화하도록 변경하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java`:
- Around line 132-146: sendResetEmail currently allows repeated sends because it
always generates and sets a new code; check Redis for an existing key before
generating/storing to enforce rate limiting: in sendResetEmail, compute the key
using emailProperties.getKeyPrefix().getReset() + email, call
redisTemplate.hasKey(key) (or opsForValue().get(key)) and if it exists
throw/return a clear rate-limit exception (e.g., a custom
TooManyRequestsException or reuse MailSendException with a specific message)
instead of proceeding, otherwise generate the code via generateCode(), set it
with redisTemplate.opsForValue().set(key, code,
emailProperties.getCodeExpire()), then continue to createResetMessage/send and
keep the existing catch that deletes the key on failure.

In
`@backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java`:
- Around line 122-126: The confirmReset endpoint currently accepts newPassword
as a `@RequestParam` (in UserController.confirmReset), which exposes sensitive
data; change the API to accept a single `@RequestBody` DTO (e.g., new
PasswordResetConfirmRequest with fields email, studentId, code, newPassword) and
update the method signature to take that DTO instead of separate
`@RequestParam/`@RequestBody mix; then pass the DTO values to
userService.resetPasswordByCode (or add an overload that accepts the DTO) and
remove reading newPassword from request params so all sensitive fields are only
in the request body.

In
`@backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetConfirmRequest.java`:
- Around line 29-30: PasswordResetConfirmRequest’s newPassword field currently
only has `@NotBlank` so a blank string can pass; add stronger validation
annotations to enforce minimum length and complexity (e.g., `@Size`(min=8) and/or
`@Pattern` with a regex requiring letters, digits and special chars) on the String
newPassword field, or attach a custom password-strength validator annotation,
and update any validation messages accordingly so
PasswordResetConfirmRequest.newPassword fails validation when it doesn't meet
the required strength.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Around line 128-147: The log statement in UserService that prints redisKey,
inputCode, savedCode, and ttl exposes sensitive reset codes; remove printing of
inputCode and savedCode (do not log codes in plain or at info level). Keep only
non-sensitive context (e.g., redisKey or a masked indicator) if needed, and if
ttl is required for diagnostics, call redisTemplate.getExpire(redisKey,
TimeUnit.SECONDS) only when actually logging TTL and log it at debug level;
otherwise remove the getExpire() call entirely. Update the log line around
redisKey/inputCode/savedCode/ttl and ensure no other code paths output savedCode
or inputCode in logs.

---

Outside diff comments:
In
`@backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java`:
- Line 10: 보안 화이트리스트에 있는 와일드카드 패턴 "/api/user/password/reset/**"가 이미
"/api/user/password/reset/confirm" 및 "/api/user/password/reset/send"를 포함해 중복되므로,
SecurityConstants 클래스에서 해당 와일드카드 항목을 제거하고 대신 명시적 경로만 유지하도록 수정하세요: 삭제 대상은
"/api/user/password/reset/**"이고 유지/추가할 명시적 항목은
"/api/user/password/reset/confirm" 및 "/api/user/password/reset/send"이며 중복된 항목(현재
22-23번에 해당하는 라인)은 제거해 향후 하위 엔드포인트가 의도치 않게 화이트리스트에 포함되는 것을 방지하세요.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Around line 168-174: The log call in validateNotBlank currently logs only
fieldName (log.debug(fieldName)), which is not descriptive; update the logging
to include a clear message and the field identifier (e.g., "Validation failed:
field is blank - {}" with fieldName) so it's obvious which field failed before
throwing CustomException(ErrorCode.INVALID_INPUT); keep using validateNotBlank
as the location and ensure log.debug includes both context and the fieldName
(and optionally the trimmed value or null) for better diagnostics.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`:
- Around line 339-346: Add unit tests in UserServiceTest covering the
password-reset flow for the newly added UserService methods
passwordResetSendCode and resetPasswordByCode: create tests that (1) verify
successful code sending for a valid email+studentId by mocking the
UserRepository to return a user and asserting MailService.send (or equivalent)
is called and any code store is populated, (2) verify sending fails (and
appropriate exception is thrown) when email+studentId does not exist, (3) verify
successful resetPasswordByCode when a valid code is provided by mocking the code
lookup to match and asserting PasswordEncoder.encode is used and
UserRepository.save is called with updated password, and (4) verify
resetPasswordByCode throws for expired/invalid codes by mocking the code lookup
to return no match or expired entry; reference
UserService.passwordResetSendCode, UserService.resetPasswordByCode,
UserRepository, MailService (or mail sending collaborator), and PasswordEncoder
when wiring mocks and assertions.

---

Nitpick comments:
In `@backend/build.gradle`:
- Around line 71-73: After adding implementation
'org.springframework.boot:spring-boot-starter-quartz', there are two consecutive
blank lines; remove one so there is only a single blank line following that
dependency to match the file's existing dependency-group spacing and keep
formatting consistent around the implementation
'org.springframework.boot:spring-boot-starter-quartz' line.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/config/AttendanceQuartzConfig.java`:
- Around line 48-60: AutowiringSpringBeanJobFactory.createJobInstance calls
beanFactory.createBean(jobClass) which creates a new instance per execution so
the AttendanceRoundStateJob `@Component` is redundant and registers an unused
singleton; remove the `@Component` annotation from the AttendanceRoundStateJob
class (or change its scope to prototype if you intentionally want singleton vs
per-execution behavior) and verify no other code depends on the bean by
searching for usages of AttendanceRoundStateJob (e.g., rg -n
"AttendanceRoundStateJob"); leave
AutowiringSpringBeanJobFactory.createJobInstance and beanFactory.createBean
as-is.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java`:
- Around line 61-89: The controller method annotated with the "출석 상태 수정"
`@Operation` in AttendanceController lacks a controller-level security check; add
a Spring Security annotation (e.g., `@PreAuthorize`) to that controller method to
enforce only session administrators/OWNER can call it (use the same
role/authority names used elsewhere in the project, e.g., hasRole('OWNER') or
hasAuthority('SESSION_ADMIN')). Import the PreAuthorize annotation and place it
directly above the method that contains the shown Operation documentation so
authorization is enforced at the controller layer rather than relying solely on
the service layer.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java`:
- Around line 140-146: In AttendanceRoundService, inside the try block that
calls RollingQrTokenUtil.verifyAndParse(qrToken, round.getQrSecret()), replace
the two separate catch blocks for IllegalArgumentException and
IllegalStateException with a single multi-catch (IllegalArgumentException |
IllegalStateException e) that throws new
CustomException(ErrorCode.QR_TOKEN_MALFORMED); this simplifies the code while
preserving identical error handling for both exceptions.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundStateJob.java`:
- Around line 23-30: 현재 AttendanceRoundStateJob.execute에서
attendanceRoundService.runRoundStatusMaintenance() 실패 시 예외를 로그만 남기고 무시하고 있으니,
catch 블록에서 로그 후 Quartz가 인지하도록 JobExecutionException을 던지도록 수정하고(예: throw new
JobExecutionException(e)), 필요하면 실패 카운터를 증가시키거나 알림을 트리거하는 로직을 추가해 운영 모니터링에 반영하세요;
대상 식별자: AttendanceRoundStateJob.execute,
attendanceRoundService.runRoundStatusMaintenance, JobExecutionException.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java`:
- Around line 171-172: Extract the magic literal 5L used to compute the default
allowed minutes into a descriptive constant (e.g. private static final long
DEFAULT_ALLOWED_MINUTES = 5L) and replace the inline 5L in AttendanceService
where minutes is computed ((allowedMinutes == null || allowedMinutes <= 0) ?
DEFAULT_ALLOWED_MINUTES : allowedMinutes.longValue()), leaving the subsequent
use of round.getStartAt().plusMinutes(minutes) unchanged.
- Around line 168-169: Decide on the true nullability of AttendanceSession and
make accesses consistent: if AttendanceSession is guaranteed non-null by the
entity mapping, remove the redundant null check in decideLate and replace it
with a non-null contract (e.g., use Objects.requireNonNull or let it surface as
an NPE) so checkIn, getAttendancesByRound, updateAttendanceStatusByRound and
decideLate all access round.getAttendanceSession() directly; otherwise, if it
can be null, add the same null-guard/handling currently in decideLate to
checkIn, getAttendancesByRound, and updateAttendanceStatusByRound (or throw a
clear IllegalStateException) so every method (checkIn, getAttendancesByRound,
updateAttendanceStatusByRound, decideLate) uniformly handles
round.getAttendanceSession() being null.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java`:
- Line 47: 필드 scheduler(실타입 ScheduledExecutorService)를 생성할 때 하드코딩된 크기 4를 사용하지 말고
구성값 또는 활성 라운드 수에 따라 동적으로 결정하도록 변경하세요; 예를 들어 QrTokenStreamService에서
Executors.newScheduledThreadPool(...) 호출을 설정값(application properties 또는 생성자
인자)으로 대체하거나 활성 라운드 수 * 2(라운드당 토큰 브로드캐스트와 핑)와 같은 식으로 계산하여 스레드풀 크기를 설정하도록 수정하고, 기존
scheduleAtFixedRate를 사용하는 스케줄링 로직은 그대로 두되 스레드풀이 부족해 지연되지 않도록 생성자 또는 팩토리에서
scheduler를 주입/초기화하도록 변경하세요.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/util/RollingQrTokenUtil.java`:
- Around line 94-103: Update the exception message thrown in
RollingQrTokenUtil.hmacSha256Base64Url to reflect a signing failure instead of
token parsing: replace the IllegalStateException message "QR_TOKEN_MALFORMED"
with a signing-specific message like "QR_SIGN_ERROR" (matching QrTokenUtil) so
callers can distinguish HMAC/signing errors from token parsing errors; keep the
original exception as the cause when rethrowing.
- Around line 82-92: Reduce the exposure of internal token creation logic by
changing the access modifier of buildToken so it is package-private (remove
public) to prevent external callers from forging tokens with arbitrary expiresAt
values; leave toWindowExpiresAt as-is (or make package-private only if
QrTokenStreamService does not require public access) and update any related
Javadoc/comments accordingly; locate the methods by their names to apply the
modifier change: buildToken(...) and toWindowExpiresAt(...), and ensure
tests/uses in QrTokenStreamService still compile after the change.
- Around line 105-113: Replace the custom constant-time comparison in
constantTimeEquals with Java's built-in constant-time method: convert the two
input Strings to byte[] using a stable charset (e.g., StandardCharsets.UTF_8)
and call MessageDigest.isEqual(byte[], byte[]) to perform the comparison; keep
the null checks and length handling as needed (or rely on MessageDigest.isEqual
after null/length checks) so callers of constantTimeEquals (the method
constantTimeEquals) get the same constant-time guarantee without a custom
bitwise loop.

In
`@backend/src/main/java/org/sejongisc/backend/common/auth/controller/AuthCookieHelper.java`:
- Around line 30-32: Replace the commented local-only cookie settings in
AuthCookieHelper by externalizing cookie config into environment-specific
properties (e.g., add cookie.secure and cookie.same-site in application.yml /
application-local.yml) and read them via `@Value` or a `@ConfigurationProperties`
bean; then apply those properties wherever cookies are created/removed
(references: AuthCookieHelper, deleteCookie, the secure(...) and sameSite(...)
usages) so local profile overrides (`@Profile`("local") or application-local.yml)
control secure/same-site behavior instead of committing commented code.

In
`@backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java`:
- Around line 132-145: Add the same error logging used in sendEmail to
sendResetEmail: inside the catch block for MessagingException | MailException in
sendResetEmail, call log.error with a descriptive message and the caught
exception (e.g., "Failed to send reset email for {}" with the email and
exception e) before deleting the Redis key and throwing the MailSendException;
reference sendResetEmail, createResetMessage, redisTemplate, mailSender,
MailSendException and reuse the logging pattern used in sendEmail.

In
`@backend/src/main/java/org/sejongisc/backend/common/config/swagger/SwaggerConfig.java`:
- Around line 5-8: In SwaggerConfig, replace the fully-qualified Server usages
in the openAPI() builder with the imported Server type (use new Server() instead
of io.swagger.v3.oas.models.servers.Server) so the import of Server is actually
used, and either add an .info(...) call using the imported Info (e.g., .info(new
Info().title(...).version(...).description(...)) in openAPI()) or remove the
unused Info import if API metadata is not needed; update the openAPI() method
accordingly to reflect one of these two choices.
- Around line 36-38: In SwaggerConfig, the new
io.swagger.v3.oas.models.servers.Server() is left with .url() commented out
which causes the Swagger UI to show a blank/null server; fix it by either
removing that Server entry or setting a placeholder/prod URL and preferably
wiring it from configuration: add a String serverUrl injected via
`@Value`("${swagger.server.url:}") (or from Environment/profile) and call
.url(serverUrl) when building the Server instance in the OpenAPI/OpenAPI bean
initializer in SwaggerConfig; ensure the server is omitted entirely if serverUrl
is empty or set a clear placeholder like "http://localhost:8080" for dev
profiles.

In
`@backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetConfirmRequest.java`:
- Around line 26-27: PasswordResetConfirmRequest is missing `@Schema` annotations
on the studentId and newPassword fields; add `@Schema`(...) to both fields
consistent with the existing email and code annotations (providing a description
and example as used for email/code) and ensure the
io.swagger.v3.oas.annotations.media.Schema import is present so Swagger docs
remain consistent for the class.

In
`@backend/src/main/java/org/sejongisc/backend/user/dto/PasswordResetSendRequest.java`:
- Around line 16-17: The PasswordResetSendRequest DTO is missing a `@Schema`
annotation on the studentId field which breaks Swagger consistency; update the
PasswordResetSendRequest class to annotate the studentId field (String
studentId) with a `@Schema` providing a description, example and required flag
consistent with the existing email field's `@Schema` usage so OpenAPI docs show
the same metadata for both fields.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Around line 104-113: In passwordResetSendCode, avoid logging raw PII in the
debug log; change the log.debug call that currently references nEmail and
nStudentId (after userRepository.existsByEmailAndStudentId check) to log masked
values instead. Implement or call a small masking helper (e.g., maskEmail and
maskStudentId) to obfuscate the local-part and/or domain of nEmail and to
partially hide nStudentId (e.g., show only last 2 characters), then pass those
masked strings into log.debug so you still get useful context without storing
plaintext PII.
- Around line 176-203: Remove the dead Redis helper methods
saveResetTokenToRedis, getEmailFromRedis, and deleteResetTokenFromRedis from
UserService since they are no longer used; before deleting, search for any
callers and update or remove them if found, then run a build to ensure no
references remain. Also remove the unnecessary cast in getEmailFromRedis (the
(String) cast) if you decide to keep similar logic elsewhere, and ensure
redisTemplate is typed as RedisTemplate<String,String> where appropriate.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`:
- Around line 135-168: Remove the entire commented-out test block for
signup_nullRole_defaultsToMember in UserServiceTest: delete the commented `@Test`
method (including the SignupRequest setup, mocked when(...) stubs, save Answer
lambda, and assertions) and commit the cleanup; if the test is needed later,
recover it from Git history rather than leaving it commented in the file, and
ensure any behavior around userService.signup and Role defaults is covered by an
active test instead.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java (2)

39-63: ⚠️ Potential issue | 🔴 Critical

UserService의 새 의존성(RedisTemplate, EmailProperties)에 대한 @Mock 선언이 누락되었습니다.

UserServiceRedisTemplate<String, String>EmailProperties 필드가 추가되었으나, 이 테스트 클래스에 해당 Mock이 없습니다. @InjectMocks가 이들을 주입하지 못해 테스트가 실패합니다.

🐛 수정 제안: 누락된 Mock 추가
+    `@Mock`
+    private RedisTemplate<String, String> redisTemplate;
+
+    `@Mock`
+    private EmailProperties emailProperties;
+
     `@InjectMocks` private UserService userService;

추가로 필요한 import:

import org.sejongisc.backend.common.config.EmailProperties;
import org.springframework.data.redis.core.RedisTemplate;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`
around lines 39 - 63, Add mocks for the new UserService dependencies so
`@InjectMocks` can populate them: declare `@Mock` private RedisTemplate<String,
String> redisTemplate and `@Mock` private EmailProperties emailProperties in
UserServiceTest (matching the types added to UserService), and add the
corresponding imports for org.springframework.data.redis.core.RedisTemplate and
org.sejongisc.backend.common.config.EmailProperties; this will allow the
existing `@InjectMocks` private UserService userService to receive the new
dependencies during tests.

142-181: 🛠️ Refactor suggestion | 🟠 Major

새로운 비밀번호 재설정 흐름(passwordResetSendCode, resetPasswordByCode)에 대한 테스트가 없습니다.

핵심 기능인 비밀번호 재설정 로직이 추가되었으나 단위 테스트가 작성되지 않았습니다. 최소한 다음 시나리오에 대한 테스트를 추가해 주세요:

  • 이메일+학번 불일치 시 예외 발생
  • 인증코드 만료/불일치 시 예외 발생
  • 정상 흐름에서 비밀번호 변경 및 토큰 무효화 확인

테스트 코드 생성을 도와드릴까요, 또는 이를 추적하기 위한 이슈를 생성할까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`
around lines 142 - 181, Add unit tests to UserServiceTest covering password
reset flows for the newly added passwordResetSendCode and resetPasswordByCode
methods: (1) "email+studentId mismatch" — mock userRepository (or the lookup
used by passwordResetSendCode/resetPasswordByCode) to return empty and assert
the service throws the expected exception; (2) "code expired/mismatch" — mock
the code store (e.g., passwordResetTokenRepository or whatever the service uses)
to return a token with an expired timestamp or a different code and assert the
expected exception is thrown; (3) "successful reset" — mock a valid user, a
matching non-expired code, and passwordEncoder.encode to return an encoded
value, call resetPasswordByCode, then verify userRepository.save was called with
the new encoded password and that the service invoked the token invalidation /
session invalidation path (e.g., calls to any
invalidateTokens/invalidateSessions/deleteResetToken methods used by the
service). Use the existing mocking style in UserServiceTest
(when(...).thenReturn(...), when(...).thenAnswer(...), verify(...)) and
reference passwordResetSendCode and resetPasswordByCode to locate where to add
these tests.
🧹 Nitpick comments (1)
backend/src/main/java/org/sejongisc/backend/user/service/UserService.java (1)

209-226: getEmailFromRedisdeleteResetTokenFromRedis 메서드를 제거하세요.

이 두 메서드는 호출되지 않는 데드 코드입니다. 토큰 기반 비밀번호 재설정 흐름에서 사용되던 메서드로, 현재 Redis 코드 기반 흐름으로 완전히 전환되면서 더 이상 사용되지 않습니다. 코드의 명확성을 위해 제거하시기 바랍니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`
around lines 209 - 226, Remove the two dead private methods
getEmailFromRedis(String token) and deleteResetTokenFromRedis(String token) from
UserService.java since they are no longer referenced by the password-reset flow;
delete their method definitions (including any method-level comments) and remove
any now-unused imports or private helper references that were only used by these
methods to avoid compiler warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java`:
- Around line 128-141: sendResetEmail currently references a non-existent
redisTemplate field causing compilation errors; replace those calls to use the
already-injected redisService (i.e., change redisTemplate.opsForValue().set(...)
to redisService.set(key, code, emailProperties.getCodeExpire()) and
redisTemplate.delete(key) to redisService.delete(key)), ensuring you keep the
existing key generation (emailProperties.getKeyPrefix().getReset() + email) and
the try/catch around createResetMessage/send via mailSender; alternatively, if
you prefer to keep redisTemplate, add a private final
RedisTemplate<String,String> redisTemplate and wire it like other injected
fields, but prefer the redisService replacement to match surrounding methods.

In
`@backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java`:
- Around line 20-22: In SecurityConstants (the list/array of public URL
patterns), fix the missing comma after the string literal
"/api/user/password/reset/send" so the Java array compiles, and remove the
redundant entries "/api/user/password/reset/confirm" and
"/api/user/password/reset/send" because "/api/user/password/reset/**" already
covers them; alternatively, if you prefer explicit paths, keep the explicit
entries and ensure each string in the array is separated by a comma.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Line 46: UserService 클래스의 필드 redisTemplate 선언에 대한 컴파일 오류는 RedisTemplate
import가 누락되어 발생하므로, 클래스 상단에 org.springframework.data.redis.core.RedisTemplate를
import하도록 추가하여 UserService 내 private final RedisTemplate<String, String>
redisTemplate; 선언이 정상적으로 컴파일되게 수정하세요.

In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`:
- Around line 235-303: Remove the orphan block-comment terminator "*/" left at
the end of the commented OAuth tests in UserServiceTest.java: locate the
commented test methods findOrCreateUser_existingUser and
findOrCreateUser_newUser and delete the stray "*/" that follows them (or delete
the entire commented block) so the file compiles; ensure no unmatched /* or */
remain after the change.

---

Outside diff comments:
In
`@backend/src/test/java/org/sejongisc/backend/user/service/UserServiceTest.java`:
- Around line 39-63: Add mocks for the new UserService dependencies so
`@InjectMocks` can populate them: declare `@Mock` private RedisTemplate<String,
String> redisTemplate and `@Mock` private EmailProperties emailProperties in
UserServiceTest (matching the types added to UserService), and add the
corresponding imports for org.springframework.data.redis.core.RedisTemplate and
org.sejongisc.backend.common.config.EmailProperties; this will allow the
existing `@InjectMocks` private UserService userService to receive the new
dependencies during tests.
- Around line 142-181: Add unit tests to UserServiceTest covering password reset
flows for the newly added passwordResetSendCode and resetPasswordByCode methods:
(1) "email+studentId mismatch" — mock userRepository (or the lookup used by
passwordResetSendCode/resetPasswordByCode) to return empty and assert the
service throws the expected exception; (2) "code expired/mismatch" — mock the
code store (e.g., passwordResetTokenRepository or whatever the service uses) to
return a token with an expired timestamp or a different code and assert the
expected exception is thrown; (3) "successful reset" — mock a valid user, a
matching non-expired code, and passwordEncoder.encode to return an encoded
value, call resetPasswordByCode, then verify userRepository.save was called with
the new encoded password and that the service invoked the token invalidation /
session invalidation path (e.g., calls to any
invalidateTokens/invalidateSessions/deleteResetToken methods used by the
service). Use the existing mocking style in UserServiceTest
(when(...).thenReturn(...), when(...).thenAnswer(...), verify(...)) and
reference passwordResetSendCode and resetPasswordByCode to locate where to add
these tests.

---

Duplicate comments:
In
`@backend/src/main/java/org/sejongisc/backend/common/auth/service/EmailService.java`:
- Around line 128-141: sendResetEmail currently overwrites Redis and allows
repeated sends; change it to check-and-set atomically (use
redisTemplate.opsForValue().setIfAbsent / SETNX with the expiration from
emailProperties.getCodeExpire()) using the same key computed via
emailProperties.getKeyPrefix().getReset() + email so a preexisting key prevents
creating a new code; if setIfAbsent returns false, throw a specific rate-limit
exception (or return early) instead of proceeding to create/send the
MimeMessage, and keep the existing behavior of deleting the key only when a send
actually fails (i.e., only delete the key in the catch for MessagingException |
MailException).

In
`@backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java`:
- Around line 122-126: The confirmReset endpoint currently accepts sensitive
values (code, newPassword) as `@RequestParam` which exposes them in URLs; change
confirmReset to accept a single `@RequestBody` `@Valid` DTO (e.g.,
PasswordResetConfirmRequest) that contains code and newPassword instead of query
params, update the method signature to use that DTO and call
userService.resetPasswordByCode(dto.getCode(), dto.getNewPassword(), existing
PasswordResetSendRequest or merge payload as needed, and remove `@RequestParam`
usage; make sure PasswordResetConfirmRequest has appropriate validation
annotations and that any downstream service method signatures are adjusted to
accept the values from the body rather than request params.

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Around line 131-138: Remove printing sensitive codes and avoid unnecessary
Redis TTL call: stop logging inputCode and savedCode in the log.info call inside
UserService (the block that computes redisKey), and remove the separate
redisTemplate.getExpire(redisKey, ...) call if its only purpose was for logging;
instead log non-sensitive, minimal info (e.g., existence or masked status) and
check TTL only when needed for business logic. Update references around
redisTemplate, emailProperties.getKeyPrefix().getReset(), redisKey, inputCode,
savedCode and remove/replace the getExpire(...) usage so no plaintext secrets
are written to logs.

---

Nitpick comments:
In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Around line 209-226: Remove the two dead private methods
getEmailFromRedis(String token) and deleteResetTokenFromRedis(String token) from
UserService.java since they are no longer referenced by the password-reset flow;
delete their method definitions (including any method-level comments) and remove
any now-unused imports or private helper references that were only used by these
methods to avoid compiler warnings.

@daye200 daye200 linked an issue Feb 21, 2026 that may be closed by this pull request
Copy link
Contributor

@discipline24 discipline24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚙️ [기능추가][로그인] 비밀번호 초기화 API 구현

2 participants